package main

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"syscall"
	"time"
	"unsafe"

	"github.com/kardianos/osext"
)

type dirDatePair struct {
	FullPath string
	Date     time.Time
}

type dirDatePairList []dirDatePair

func (p dirDatePairList) Len() int           { return len(p) }
func (p dirDatePairList) Less(i, j int) bool { return p[i].Date.Before(p[j].Date) }
func (p dirDatePairList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p dirDatePair) String() string         { return "{" + p.FullPath + ": " + p.Date.String() + "}" }

type config struct {
	FreeGbRequired int
	TestMode       bool
	DaysToKeep     int
	DaysToDelete   int
	WriteLog       bool
	LogFile        string
	Dirs           [][]dirsToScan
}

type dirsToScan struct {
	Pattern    string
	FullPath   string
	TotalLevel int
}

func parseDir(pattern string, rootDir string, currentLevel int, totalLevel int) (ret dirDatePairList) {

	reg := regexp.MustCompile(pattern)
	ret = make(dirDatePairList, 0)

	dirs, _ := ioutil.ReadDir(rootDir)
	for _, dir := range dirs {
		fullPath := filepath.Join(rootDir, dir.Name())

		if currentLevel == totalLevel {
			matchedGroups := reg.FindStringSubmatch(fullPath)
			if len(matchedGroups) == 4 {
				y, _ := strconv.Atoi(matchedGroups[1])
				m, _ := strconv.Atoi(matchedGroups[2])
				d, _ := strconv.Atoi(matchedGroups[3])
				t := time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Local)
				pair := dirDatePair{fullPath, t}
				ret = append(ret, pair)
			} else {
				log.Println(fullPath + " can't be parsed")
			}
		} else if currentLevel < totalLevel {
			sub := parseDir(pattern, fullPath, currentLevel+1, totalLevel)
			ret = append(ret, sub...)
		}
	}

	return
}

func getFreeSpaceInGb(disk string) int {
	h := syscall.MustLoadDLL("kernel32.dll")
	c := h.MustFindProc("GetDiskFreeSpaceExW")
	lpFreeBytesAvailable := int64(0)
	lpTotalNumberOfBytes := int64(0)
	lpTotalNumberOfFreeBytes := int64(0)
	c.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(disk))),
		uintptr(unsafe.Pointer(&lpFreeBytesAvailable)),
		uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
		uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes)))
	return int(lpTotalNumberOfFreeBytes / 1024 / 1024 / 1024)
}

func delOldest(dirs []dirsToScan, daysToKeep int, isTest bool, writeLog bool) {
	list := make(dirDatePairList, 0)
	for _, v := range dirs {
		subList := parseDir(v.Pattern, v.FullPath, 1, v.TotalLevel)
		list = append(list, subList...)
	}

	sort.Sort(list)
	//fmt.Println(list)
	if len(list) > 0 {
		y, m, d := time.Now().Date()
		before := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, -daysToKeep)
		oldest := list[0]
		if oldest.Date.Before(before) {
			for _, v := range list {
				if v.Date == oldest.Date {
					msg := "Will Remove: " + v.FullPath
					log.Println(msg)
					if !isTest {
						cmd := exec.Command("cmd", "/C", "rd /s /q", v.FullPath)
						err := cmd.Run()
						if err != nil {
							log.Println("Error remove dir: "+v.FullPath, err)
							if writeLog {
								log.Println("Error remove dir: "+v.FullPath, err)
							}
						} else {
							if writeLog {
								log.Println(msg)
							}
						}
					}
				} else {
					log.Println("Skip: " + v.FullPath)
				}
			}
		} else {
			daysBetween := int64(time.Now().Sub(oldest.Date).Hours() / 24)
			log.Printf("The oldest day: %v (%d day(s) old) is not old enough: %d day(s)\n", oldest.Date, daysBetween, daysToKeep)
		}
	}

}

func readConfig() (cfg config, err error) {
	exeFolder, err := osext.ExecutableFolder()
	if err != nil {
		log.Fatal(err)
	}

	path := filepath.Join(exeFolder, "config.json")
	file, err := os.Open(path)
	if err != nil {
		log.Fatal(err)
	}
	decoder := json.NewDecoder(file)
	cfg = config{}
	err = decoder.Decode(&cfg)
	return

}

func main() {

	cfg, err := readConfig()
	if err != nil {
		log.Fatal(err)
	}

	//fmt.Printf("%v\n", cfg)

	//setup logging
	logFile, err := os.OpenFile(cfg.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0)
	if err != nil {
		log.Fatal("can't open ", cfg.LogFile, err)
	}
	defer logFile.Close()

	multiTarget := io.MultiWriter(os.Stdout, logFile)

	log.SetFlags(log.LstdFlags)
	log.SetOutput(multiTarget)

	//main logic
	for _, dirList := range cfg.Dirs {
		if len(dirList) > 0 {
			disk := dirList[0].FullPath[:2]
			if getFreeSpaceInGb(disk) < cfg.FreeGbRequired {
				for i := 0; i < cfg.DaysToDelete; i++ {
					delOldest(dirList, cfg.DaysToKeep, cfg.TestMode, cfg.WriteLog)
				}
			} else {
				log.Println("disk " + disk + " free space is enough, skip")
			}
		}
	}

}
